A simple backend service that manages wallet transactions, built with Express, SQLite, and Drizzle ORM.
# Install dependencies
pnpm install
# Run database migrations
pnpm db:migrate
# Start the dev server (with hot reload)
pnpm dev
The API will be available at http://localhost:3000.
To populate the database with sample accounts:
curl -X POST http://localhost:3000/api/seed
This creates 3 accounts (Alice, Bob, Charlie) with pre-funded balances and resets the DB each time.
| Method | Endpoint | Description |
|---|---|---|
| GET | /health |
Health check |
| POST | /api/seed |
Seed DB with test data |
| GET | /api/accounts |
List all accounts with balances |
| POST | /api/accounts |
Create a new account |
| GET | /api/accounts/:id |
Get account details |
| POST | /api/accounts/:id/deposit |
Deposit funds |
| POST | /api/accounts/:id/withdraw |
Withdraw funds |
| POST | /api/transfers |
Transfer between accounts |
| GET | /api/accounts/:id/transactions |
Transaction history (paginated) |
curl -X POST http://localhost:3000/api/accounts \
-H "Content-Type: application/json" \
-d '{"name": "Alice", "email": "alice@example.com"}'
{
"success": true,
"data": {
"id": 1,
"name": "Alice",
"email": "alice@example.com",
"balance": 0,
"createdAt": "2026-03-15 12:00:00",
"updatedAt": "2026-03-15 12:00:00"
}
}
curl http://localhost:3000/api/accounts
curl -X POST http://localhost:3000/api/accounts/1/deposit \
-H "Content-Type: application/json" \
-d '{"amount": 150.00, "description": "Initial deposit", "reference": "dep-001"}'
{
"success": true,
"data": {
"transaction": {
"id": "uuid",
"type": "deposit",
"amount": 150,
"senderId": null,
"receiverId": 1,
"reference": "dep-001",
"description": "Initial deposit",
"createdAt": "2026-03-15 12:00:00"
},
"newBalance": 150
}
}
curl -X POST http://localhost:3000/api/accounts/1/withdraw \
-H "Content-Type: application/json" \
-d '{"amount": 50.00, "description": "ATM withdrawal", "reference": "wd-001"}'
curl -X POST http://localhost:3000/api/transfers \
-H "Content-Type: application/json" \
-d '{"senderId": 1, "receiverId": 2, "amount": 50.00, "description": "Payment", "reference": "txn-001"}'
{
"success": true,
"data": {
"transaction": {
"id": "uuid",
"type": "transfer",
"amount": 50,
"senderId": 1,
"receiverId": 2,
"reference": "txn-001",
"description": "Payment",
"createdAt": "2026-03-15 12:00:00"
},
"senderBalance": 100,
"receiverBalance": 50
}
}
curl "http://localhost:3000/api/accounts/1/transactions?page=1&limit=10"
All monetary values are stored internally as integers in cents to avoid floating-point precision issues. The API accepts and returns decimal dollar values (e.g., 150.00), converting at the service layer boundary.
Every transaction requires a unique reference string. If a client retries a request with the same reference, the API returns a 409 Conflict instead of creating a duplicate transaction.
Transfers use SQLite transactions to ensure debit + credit + transaction record creation are all-or-nothing. If any step fails, the entire operation rolls back.
Account IDs are auto-incrementing integers for ease of manual testing (/api/accounts/1 vs UUID strings).
All error responses follow a consistent format:
{
"success": false,
"error": "Description of what went wrong"
}
| Status | Condition |
|---|---|
| 400 | Invalid input / validation error |
| 404 | Account not found |
| 409 | Duplicate reference |
| 422 | Insufficient balance |
| 500 | Unexpected server error |
hljs src/
index.ts # Express app entry point
config/
database.ts # Drizzle + SQLite setup
env.ts # Environment config
migrate.ts # Migration runner
db/schema/
index.ts # Drizzle table definitions
types/ # TypeScript DTOs and interfaces
repositories/ # Database access layer
services/ # Business logic layer
controllers/ # HTTP request handlers
routes/ # Express route definitions
middleware/ # Validation & error handling
utils/ # Helpers (errors, money conversion)
Follows Clean Architecture: Controllers (HTTP) -> Services (business logic) -> Repositories (DB).
This is a minimal implementation focused on the core task requirements: account management, deposits, withdrawals, transfers, and transaction history with idempotency and atomicity guarantees.
The following were intentionally left out to keep scope tight:
console.error on unhandled exceptions..env.